/*
 * jxUtils Project (2014)
 * 
 * by Denilson Edinaldo Pinto
 * 
 * LGPL
 * Copyright (C) 2014
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.jxUtils;

import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;

import org.jxUtils.check.CheckUtil;
import org.jxUtils.check.ParameterCheck;

import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;
import sun.reflect.generics.reflectiveObjects.TypeVariableImpl;

/**
 * @author Denilson Edinaldo Pinto
 * @porject jxUtils
 * @package org.jxUtils
 * @date 24/01/2014 - 19:20:01
 */
public final class ReflectionUtil {
	
	/**
	 * @param object
	 * @return copy or null
	 */
	@SuppressWarnings("unchecked")
	public static <T extends Serializable> T clone(T object) {
	
		// 
		if(object == null) {
			
			return null;
		}
		
		ObjectOutputStream outputStream = null;
		ObjectInputStream inputStream = null;
		
		ByteArrayOutputStream byteArrayOutputStream = null;
		ByteArrayInputStream byteArrayInputStream = null;
		
		try {
			//
			byteArrayOutputStream = new ByteArrayOutputStream();
			
			outputStream = new ObjectOutputStream(byteArrayOutputStream);
			outputStream.writeObject(object);
			
			//
			byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
			
			inputStream = new ObjectInputStream(byteArrayInputStream);
			T copy = (T)inputStream.readObject();
			
			return copy;
			
		}catch(Throwable throwable) {
			
			throwable.printStackTrace();
			
		}finally {
			
			try {
				//
				if(byteArrayOutputStream != null) {
					
					byteArrayOutputStream.close();
				}
				
				//
				if(outputStream != null) {
					
					outputStream.close();
				}
				
				//
				if(byteArrayInputStream != null) {
					
					byteArrayInputStream.close();
				}
				
				//
				if(inputStream != null) {
					
					inputStream.close();
				}
				
			}catch(IOException ioException) {
				// ignore
			}
		}
		
		return null;
	}
	
	/**
	 * @param object
	 * @param methodStartWith
	 * @return the methods list
	 */
	public static List<Method> getAllMethods(final Object object, final String methodStartWith) {
	
		ParameterCheck.isNull(object, "object");
		
		List<Method> methods = new ArrayList<Method>();
		
		String methodStartWithAux = CheckUtil.isNullOrAbsoluteEmpty(methodStartWith) ? "" : methodStartWith.toLowerCase();
		
		//
		for(Method method : object.getClass().getDeclaredMethods()) {
			
			//
			if(method.getName().toLowerCase().startsWith(methodStartWithAux)) {
				
				methods.add(method);
			}
		}
		
		//
		for(Method method : object.getClass().getMethods()) {
			
			//
			if(method.getName().toLowerCase().startsWith(methodStartWithAux)) {
				
				methods.add(method);
			}
		}
		
		return methods;
	}
	
	/**
	 * @param object
	 * @param field
	 * @return Field Name or null
	 * @throws Throwable
	 */
	public static String getFieldName(final Object object, final Object field) throws Throwable {
	
		ParameterCheck.isNull(object, "object");
		ParameterCheck.isNull(field, "field");
		
		// Para cada Propriedade do Objeto
		for(final PropertyDescriptor prop : Introspector.getBeanInfo(object.getClass()).getPropertyDescriptors()) {
			
			//
			if( !prop.getName().equals("class")) {
				
				//
				if(prop.getReadMethod() != null) {
					
					//
					if(field.equals(prop.getReadMethod().invoke(object))) {
						
						return prop.getName();
					}
				}
			}
		}
		
		return null;
	}
	
	/**
	 * @param object
	 * @param returnClasstype
	 * @param methodStartWith
	 * @return the method or null
	 */
	public static Method getFirstMethod(final Object object, final Class<?> returnClasstype, final String methodStartWith) {
	
		ParameterCheck.isNull(returnClasstype, "returnClasstype");
		
		//
		for(Method method : getAllMethods(object, methodStartWith.toLowerCase())) {
			
			//
			if(method.getReturnType().equals(returnClasstype)) {
				
				return method;
			}
		}
		
		return null;
	}
	
	/**
	 * @param object
	 * @param returnClasstype
	 * @return the get method or null
	 */
	public static Method getFirstMethodGET(final Object object, final Class<?> returnClasstype) {
	
		return getFirstMethod(object, returnClasstype, "GET");
	}
	
	/**
	 * @param object
	 * @param returnClasstype
	 * @return the set method or null
	 */
	public static Method getFirstMethodSET(final Object object, final Class<?> returnClasstype) {
	
		return getFirstMethod(object, returnClasstype, "SET");
	}
	
	/**
	 * @param object
	 * @param methodName
	 * @return Method or null
	 */
	public static Method getMethod(final Object object, final String methodName) {
	
		ParameterCheck.isNull(object, "object");
		ParameterCheck.isNullOrAbsoluteEmpty(methodName, "methodName");
		
		//
		for(final Method method : object.getClass().getMethods()) {
			
			//
			if(method.getName().equalsIgnoreCase(methodName)) {
				
				return method;
			}
		}
		
		return null;
	}
	
	/**
	 * @param object
	 * @param methodName
	 * @return Get Method or null
	 */
	public static Method getMethodGet(final Object object, final String methodName) {
	
		ParameterCheck.isNull(object, "object");
		ParameterCheck.isNullOrAbsoluteEmpty(methodName, "methodName");
		
		//
		if( !methodName.toLowerCase().startsWith("get") && !methodName.toLowerCase().startsWith("is")) {
			
			Method method = getMethod(object, "get" + methodName);
			
			//
			if(method == null) {
				
				method = getMethod(object, "is" + methodName);
			}
			
			return method;
			
		}else {
			
			return getMethod(object, methodName);
		}
	}
	
	/**
	 * @param object
	 * @param methodName
	 * @return Set Method or null
	 */
	public static Method getMethodSet(final Object object, final String methodName) {
	
		ParameterCheck.isNull(object, "object");
		ParameterCheck.isNullOrAbsoluteEmpty(methodName, "methodName");
		
		//
		if( !methodName.toLowerCase().startsWith("set") && !methodName.toLowerCase().startsWith("is")) {
			
			Method method = getMethod(object, "set" + methodName);
			
			//
			if(method == null) {
				
				method = getMethod(object, "is" + methodName);
			}
			
			return method;
			
		}else {
			
			return getMethod(object, methodName);
		}
	}
	
	/**
	 * @param clazz
	 * @return parameter classes from Generic Superclass
	 */
	public static List<Class<?>> getParameterClass(final Class<?> clazz) {
	
		ParameterCheck.isNull(clazz, "clazz");
		
		final List<Class<?>> classes = new ArrayList<>();
		
		//
		if(clazz.getGenericSuperclass() instanceof ParameterizedType) {
			
			// Obtém a Classe do parâmetro Bean
			final Type[] types = ((ParameterizedType)clazz.getGenericSuperclass()).getActualTypeArguments();
			
			//
			for(final Type type : types) {
				
				//
				if(type instanceof TypeVariableImpl<?>) {
					
					final TypeVariableImpl<?> implBean = (TypeVariableImpl<?>)type;
					
					classes.add((Class<?>)implBean.getBounds()[0]);
					
				}else if(type instanceof ParameterizedTypeImpl) {
					
					classes.add(((ParameterizedTypeImpl)type).getRawType());
					
				}else {
					
					classes.add((Class<?>)type);
				}
			}
			
		}else {
			
			classes.add((Class<?>)clazz.getGenericSuperclass());
		}
		
		return classes;
	}
	
	/**
	 * @param object
	 * @param methodName
	 * @return Class or null
	 */
	public static <Bean> Class<?> getReturnType(final Bean object, final String methodName) {
	
		ParameterCheck.isNull(object, "object");
		ParameterCheck.isNullOrAbsoluteEmpty(methodName, "methodName");
		
		final Method method = getMethodGet(object, methodName);
		
		return method != null ? method.getReturnType() : null;
	}
	
	/**
	 * @param <Bean>
	 * @param object
	 * @param methodName
	 * @param values
	 * @throws InvocationTargetException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	public static <Bean> void setValue(final Bean object, final String methodName, final Object... values) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
	
		ParameterCheck.isNull(object, "object");
		ParameterCheck.isNullOrAbsoluteEmpty(methodName, "methodName");
		
		final Method methodSet = getMethodSet(object, methodName);
		
		//
		if(methodSet != null) {
			
			methodSet.invoke(object, values);
		}
	}
}
